package com.young.common.client.log.web;

import com.young.common.client.log.configuration.FileLogCollectConfiguration;
import com.young.common.client.log.model.LogContent;
import com.young.common.client.log.model.LogFile;
import com.young.common.client.log.model.LogLine;
import com.young.common.util.CommonUtil;
import com.young.common.util.FileDownloadUtil;
import com.young.common.util.Result;
import com.young.common.util.StringUtils;
import com.young.interfaces.log.annotation.Log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;

/**
 * 日志客户端web
 *
 * @author imrookie
 * @date 2018/11/19
 */
@Controller
@RequestMapping(value = "/log/client", method = {RequestMethod.GET, RequestMethod.POST})
public class FileLogCollectController {

    private static final Logger logger = LoggerFactory.getLogger(FileLogCollectController.class);

    @Autowired(required = false)
    FileLogCollectConfiguration logCollectConfiguration;
    /**
     * 每次读取的最大行数
     */
    private static final int READ_LINE_MAX = 200;
    /**
     * 最大检索文件大小, 100M
     * 一次搜索达到100M就不再继续搜索了
     */
    private static final long MAX_SEARCH_SIZE_BYTES = 100 * 1024 * 1024L;
    /**
     * 向上翻页时的读取文件大小
     * 由于RandomAccessFile的api是向下读, 所以向上读取是通过预设起始位置实现的, 设置的相对小一些, 避免资源浪费
     */
    private static final long PREVIOUS_MAX_SEARCH_SIZE_BYTES = 20 * 1024 * 1024L;
    /**
     * 每行字符串长度限制
     * 超出的部分被截断
     */
    private static final int MAX_LINE_STRING_LENGTH = 5000;

    /**
     * 实时读取日志
     * @param lastPointer 上一次读取的指针位置
     * @param charset 日志文件编码方式
     * @param request
     * @return
     */
    @RequestMapping("/realTime")
    @ResponseBody
    public Object readMenu(Long lastPointer, String charset, HttpServletRequest request){
        if (logCollectConfiguration == null || logCollectConfiguration.getEnable() == null || !logCollectConfiguration.getEnable()) {
            return Result.buildFail("日志收集客户端配置无效!");
        }
        File file = null;
        if (logCollectConfiguration.isAbsolutePath()) {
            file = new File(logCollectConfiguration.getLogFileSavePath());
        } else {
            file = new File(new File(request.getServletContext().getRealPath("")), logCollectConfiguration.getLogFileSavePath());
        }

        if (!file.exists()) {
            return Result.buildFail("配置的日志文件不存在");
        }
        if (file.isDirectory()) {
            return Result.buildFail("配置的日志路径应为文件类型");
        }

        //返回的日志内容
        LogContent logContent = new LogContent();

        //第一次读,不携带lastPointer,默认从末尾开始
        if (lastPointer == null) {
            logContent.setLastPointer(file.length());//当前指针位置默认放到文件末尾
            return Result.buildSuccess(logContent);
        }

        //已经读到末尾,直接返回,等待下次读取
        if (lastPointer == file.length()) {
            logContent.setLastPointer(lastPointer);
            return Result.buildSuccess(logContent);
        }

        //检查读取进度
        if (lastPointer > file.length()) {
            return Result.buildFail("读取位置超出范围");
        }

        charset = CommonUtil.getDefaultValue(charset, "ISO-8859-1");

        try {
            RandomAccessFile randomFile = new RandomAccessFile(file, "r");
            randomFile.seek(lastPointer);
            List<LogLine> logs = new LinkedList<LogLine>();
            String logOneLine = null;
            int maxLine = READ_LINE_MAX;//最多读取xx行
            long last = lastPointer;//记录上一次读取位置
            while (maxLine > 0 && (logOneLine = randomFile.readLine()) != null) {
                String text = new String(logOneLine.getBytes(charset), "UTF-8");
                //日志超长进行截断
                if (text.length() > MAX_LINE_STRING_LENGTH) {
                    text = text.substring(0, MAX_LINE_STRING_LENGTH) + "(超长部分被截断...)";
                }
                long curPointer = randomFile.getFilePointer();
                //加入日志列表
                logs.add(new LogLine(last, curPointer, text));
                last = curPointer;//把当前位置更新给last，等待下一次循环时使用
                maxLine--;
            }
            logContent.setLastPointer(randomFile.getFilePointer());//记录当前指针位置
            logContent.setLogList(logs);
            logContent.setSize(randomFile.length());
        } catch (FileNotFoundException e) {
            logger.error("[读取实时日志] 随机文件创建异常:", e);
        } catch (IOException e) {
            logger.error("[读取实时日志] 随机文件读取异常:", e);
        }

        return Result.buildSuccess(logContent);
    }

    /**
     * 浏览日志文件(夹)
     * @param folder
     * @param request
     * @return
     */
    @RequestMapping("/logFiles")
    @ResponseBody
    public Object logFiles(String folder, HttpServletRequest request){

        if (logCollectConfiguration == null || logCollectConfiguration.getEnable() == null || !logCollectConfiguration.getEnable()) {
            return Result.buildFail("日志收集客户端配置无效!");
        }
        File file = null;
        if (logCollectConfiguration.isAbsolutePath()) {
            file = new File(logCollectConfiguration.getLogFileSavePath());
        } else {
            file = new File(new File(request.getServletContext().getRealPath("")), logCollectConfiguration.getLogFileSavePath());
        }

        if (!file.exists()) {
            return Result.buildFail("配置的日志文件不存在");
        }
        if (file.isDirectory()) {
            return Result.buildFail("配置的日志路径应为文件类型");
        }


        File rootFolder = null;
        //folder为空,返回根目录下的文件信息
        if (StringUtils.isBlank(folder)) {
            rootFolder = file.getParentFile();//日志根目录
        } else {//folder不为空,返回指定目录下的文件
            String root = file.getParentFile().getAbsolutePath().replaceAll("\\\\", "/");
            if (folder.indexOf(root) != 0) {//若入参日志不是已根目录开头,说明是非法攻击请求
                logger.warn("[浏览日志文件] 查看文件夹[{}],不属于日志根目录[{}],返回失败", folder, root);
                return Result.buildFail("配置的日志路径应为文件类型");
            }
            rootFolder = new File(folder);
        }

        if (rootFolder == null || !rootFolder.exists() || !rootFolder.isDirectory()) {
            return Result.buildFail("日志文件夹不存在!");
        }

        List<LogFile> list = new ArrayList<LogFile>();
        File[] childs = rootFolder.listFiles();
        if (childs != null && childs.length > 0) {
            for (File f : childs) {
                LogFile logFile = new LogFile();
                logFile.setName(f.getName());
                logFile.setAddr(f.getAbsolutePath().replaceAll("\\\\", "/"));
                logFile.setFolder(f.isDirectory());
                if (!f.isDirectory()) {
                    logFile.setSize(f.length());
                }
                Calendar cal = Calendar.getInstance();
                cal.setTimeInMillis(f.lastModified());
                logFile.setUpdateTime(cal.getTime());
                list.add(logFile);
            }
        }

        return Result.buildSuccess(list);
    }

    /**
     * 查看日志
     * @param path 日志路径
     * @param pointer 读取位置,当读取上一页时,该参数表示结束位置
     * @param isNext 是否向下读取
     * @param keyword 关键字
     * @param request
     * @return
     */
    @RequestMapping("/readLog")
    @ResponseBody
    public Object readLog(String path, Long pointer, Boolean isNext, String keyword, HttpServletRequest request){
        if (logCollectConfiguration == null || logCollectConfiguration.getEnable() == null || !logCollectConfiguration.getEnable()) {
            return Result.buildFail("日志收集客户端配置无效!");
        }

        if (StringUtils.isBlank(path)) {
            return Result.buildFail("日志文件路径无效");
        }

        File file = new File(path);

        if (!file.exists()) {
            return Result.buildFail("日志文件不存在");
        }
        if (file.isDirectory()) {
            return Result.buildFail("日志路径应为文件类型");
        }

        if (isNext == null) {//默认向下读取
            isNext = true;
        }

        //返回的日志内容
        LogContent logContent = new LogContent();

        if (pointer == null) {
            pointer = 0l;
        }

        if (!isNext && pointer == 0l) {//当读取上一页时,指针位置不允许为0
            return Result.buildFail("已经到达文件开头!");
        }

        //已经读到末尾,直接返回,等待下次读取
        if (isNext && pointer == file.length()) {
            return Result.buildFail("已经到达文件末尾");
        }

        //检查读取进度
        if (pointer > file.length()) {
            return Result.buildFail("读取位置超出范围");
        }

        //关键字过滤
        boolean keywordFilter = StringUtils.isNotBlank(keyword);

        try {
            RandomAccessFile randomFile = new RandomAccessFile(file, "r");
            int maxLine = READ_LINE_MAX;//最多读取xx行
            if (isNext) {//下一页
                randomFile.seek(pointer);
                List<LogLine> logs = new LinkedList<LogLine>();
                String logOneLine = null;
                logContent.setStartPointer(randomFile.getFilePointer());//记录开始读取的位置
                long last = randomFile.getFilePointer();
                while (maxLine > 0 && (logOneLine = randomFile.readLine()) != null) {
                    String text = new String(logOneLine.getBytes("ISO-8859-1"), "UTF-8");
                    if (keywordFilter && !text.contains(keyword)) {//关键字匹配失败的就丢弃
                        long curPointer = randomFile.getFilePointer();
                        if (curPointer - pointer > MAX_SEARCH_SIZE_BYTES) {
                            logger.info("[查看历史日志] 搜索达到大小限制,停止继续检索文件,本次检索开始位置={},当前位置={},限制大小(单位byte)={}", pointer, curPointer, MAX_SEARCH_SIZE_BYTES);
                            //如果已经读取超过了阈值,则结束搜索
                            break;
                        }
                        continue;
                    }
                    //日志超长进行截断
                    if (text.length() > MAX_LINE_STRING_LENGTH) {
                        text = text.substring(0, MAX_LINE_STRING_LENGTH) + "(超长部分被截断...)";
                    }
                    long curPointer = randomFile.getFilePointer();
                    //加入日志列表
                    logs.add(new LogLine(last, curPointer, text));
                    last = curPointer;//把当前位置更新给last，等待下一次循环时使用
                    maxLine--;
                }
                logContent.setLastPointer(randomFile.getFilePointer());//记录当前指针位置
                logContent.setLogList(logs);
                logContent.setSize(randomFile.length());
                logContent.setEndPointer(randomFile.getFilePointer());//记录结束读取的位置
            } else {//上一页
                long readSize = maxLine * 230;//读取大小,默认一行是230
                //如果是带关键字搜索, 则读取指定大小的文件
                if (keywordFilter) {
                    readSize = PREVIOUS_MAX_SEARCH_SIZE_BYTES;
                }
                long start = 0;//默认从0开始
                if (readSize < pointer) {
                    start = pointer - readSize;//计算读取开始位置
                }
                randomFile.seek(start);
                if (start > 0) {//如果不是从头读,则先丢弃半行
                    randomFile.readLine();//先读一行,避免返回前端是半行
                }
                start = randomFile.getFilePointer();//从新一行开始读取
                List<LogLine> logs = new LinkedList<LogLine>();//日志列表
                List<Long> points = new LinkedList<Long>();//日志所处指针位置,用于最终下发时标记真正的起始位置
                String logOneLine = null;
                logContent.setStartPointer(start);//记录开始读取的位置
                long curPoint = start;//当前位置
                long last = randomFile.getFilePointer();
                while ((logOneLine = randomFile.readLine()) != null) {
                    String text = new String(logOneLine.getBytes("ISO-8859-1"), "UTF-8");
                    if (keywordFilter && !text.contains(keyword)) {//关键字匹配失败的就丢弃
                        continue;
                    }
                    //日志超长进行截断
                    if (text.length() > MAX_LINE_STRING_LENGTH) {
                        text = text.substring(0, MAX_LINE_STRING_LENGTH) + "(超长部分被截断...)";
                    }

                    long curPointer = randomFile.getFilePointer();
                    //加入日志列表
                    logs.add(new LogLine(last, curPointer, text));
                    last = curPointer;//把当前位置更新给last，等待下一次循环时使用

                    points.add(curPoint);//纪录该行的起始位置
                    curPoint = randomFile.getFilePointer();//更新当前位置
                    //只下发固定数量的内容
                    if (logs.size() > READ_LINE_MAX) {
                        logs.remove(0);
                        points.remove(0);
                    }
                    //当读取位置到达入参位置时,跳出
                    if (curPoint >= pointer) {
                        break;
                    }
                }
                //若成功读满日志, 则以第一条日志的位置为开始位置下发下去
                if (points.size() >= READ_LINE_MAX) {
                    logContent.setStartPointer(points.get(0));
                }
                logContent.setLastPointer(randomFile.getFilePointer());//记录当前指针位置
                logContent.setLogList(logs);
                logContent.setSize(randomFile.length());
                logContent.setEndPointer(pointer);//记录结束读取的位置, 读取上一页时, 传进来的指针就是结束指针
            }

        } catch (FileNotFoundException e) {
            logger.error("[读取历史日志] 随机文件创建异常:", e);
        } catch (IOException e) {
            logger.error("[读取历史日志] 随机文件读取异常:", e);
        }

        return Result.buildSuccess(logContent);
    }

    /**
     * 下载日志文件
     * @param path
     * @param request
     * @param response
     * @throws Exception
     */
    @Log("下载日志文件")
    @RequestMapping("/download")
    public void download(String path, HttpServletRequest request, HttpServletResponse response) throws Exception {
        logger.info("[日志文件下载] 待下载日志文件为:{}", path);
        if(StringUtils.isNotBlank(path)){
            File file = new File(path);
            if (file == null || file.isDirectory()) {
                response.getWriter().write("日志文件无效!");
            }
            String fileName = file.getName();
            FileDownloadUtil.downloadHandle(new FileInputStream(file), fileName, request, response);
            response.setHeader("Content-Length", String.valueOf(file.length()));//设置内容长度
        }else{
            response.getWriter().write("日志文件路径无效!");
        }

    }
}
